// 12.3 使用标准库：文本查询程序
/**
 * 我们将实现一个简单的文本查询程序，作为标准库相关内容学习的总结。我们的程序允许用户在一个给定文件中查询单词。
 * 查询结果是单词在文件中出现的次数及其所在行的列表。如果一个单词在一行中出现多次，此行只列出一次。
 */

#include <iterator>
#include <vector>
#include <list>
#include <deque>
#include <forward_list>
#include <string>
#include <array>
#include <stack>
#include <queue>
#include <algorithm>
#include <numeric>
using std::swap;
using std::vector, std::list, std::deque, std::forward_list, std::string, std::array, std::stack, std::queue;
#include "../Chapter07/Sales_data.h"
#include <iostream>
#include <fstream>
#include <sstream> // istringstream
using std::begin, std::cbegin, std::end, std::cend, std::find, std::accumulate, std::equal, std::fill, std::fill_n, std::back_inserter;
using std::cin, std::cout, std::endl;
using std::copy, std::replace, std::replace_copy;
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <memory>
#include <new>
using namespace std;

using line_no = vector<string>::size_type;
class QueryResult; // 为了定义query函数的返回类型，这个定义是必须的
class TextQuery {
public:
    TextQuery(std::ifstream &is);
    QueryResult query(const string &s) const;

private:
    std::shared_ptr<std::vector<std::string>> file; // 输入文件
    // 每个单词到它所在的行号的集合的映射
    std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;
};

// 读取输入文件并建立单词到行号的映射
TextQuery::TextQuery(ifstream &is) : file(new vector<string>) // file是指向vector<string>的shared_ptr（差点看成继承，突然明白过味儿来这是个构造函数，后面的是构造函数初始化列表）
{
    string text;
    while (getline(is, text))
    {
        file->push_back(text); // 保存此行文本
        int n = file->size() - 1; // 当前行号
        istringstream line(text); // 将行文本分解为单词
        string word;
        while(line >> word)    // 对行中每个单词
        {
            auto &lines = wm[word]; // lines是个shared_ptr，指向一个set<line_no>
            if(!lines) // 在我们第一次遇到这个单词时，此指针为空
                lines.reset(new set<line_no>); // 分配一个新的set
            lines->insert(n);   // 将此行号插入set中
        }
    }
}
class QueryResult {
    friend std::ostream& print(std::ostream&, const QueryResult&);
public:
    QueryResult(const string &s, shared_ptr<set<line_no>> p, shared_ptr<vector<string>> f) : sought(s), lines(p), file(f) {}

private:
    std::string sought; // 查询单词
    std::shared_ptr<set<line_no>> lines; // 出现的行号
    std::shared_ptr<vector<string>> file; // 输入文件
};

QueryResult TextQuery::query(const string &sought) const
{
    // 如果未找到sought，我们将返回一个指向此set的指针
    static shared_ptr<set<line_no>> nodata(new set<line_no>);
    // 使用find而不是下标运算符来查找单词，避免将单词添加到wm中
    auto loc = wm.find(sought); // 找到单词sought在wm中的位置
    if(loc == wm.end())
        return QueryResult(sought, nodata, file);
    else
        return QueryResult(sought, loc->second, file);
        // iter->first和iter->second是用于访问关联容器（如std::map或std::unordered_map）元素的迭代器操作。这些容器存储键值对，其中每个元素由一个键（key）和一个值（value）组成。
}

ostream &print(ostream &os, const QueryResult &qr)
{
    // 如果找到了单词，打印出现次数和所有出现的位置
    os << qr.sought << " occurs " << qr.lines->size() << (qr.lines->size() > 1? " times" : " time") << endl;
    // 打印单词出现的每一行
    for (auto num : *qr.lines)
        os << "\t(line " << num + 1 << ") " << *(qr.file->begin() + num) << endl;
    return os;
}

int main()
{
    // 12.3.1 文本查询程序设计
    // 开始一个程序的设计的一种好方法是列出程序的操作。了解需要哪些操作会帮助我们分析出需要什么样的数据结构。
    // 从需求入手，我们的文本查询程序需要完成如下任务：
    //   1.· 当程序读取输入文件时，它必须记住单词出现的每一行。因此，程序需要逐行读取输入文件，并将每一行分解为独立的单词
    //   2.· 当程序生成输出时，
    //        — 它必须能提取每个单词所关联的行号 — 行号必须按升序出现且无重复 — 它必须能打印给定行号中的文本。

    // 数据结构
    // 虽然我们可以用vector、set和map来直接编写文本查询程序，但如果定义一个更为抽象的解决方案，会更为有效。
    // 我们将从定义一个保存输入文件的类开始，这会令文件查询更为容易。我们将这个类命名为TextQuery，它包含一个vector和一个map。vector用来保存输入文件的文本，
    //   map用来关联每个单词和它出现的行号的set。这个类将会有一个用来读取给定输入文件的构造函数和一个执行查询的操作。   
    // 查询操作要完成的任务非常简单：查找map成员，检查给定单词是否出现。设计这个函数的难点是确定应该返回什么内容。一旦找到了一个单词，我们需要知道它出现了多少次、它出现的行号以及每行的文本。
    //   返回所有这些内容的最简单的方法是定义另一个类，可以命名为QueryResult，来保存查询结果。这个类会有一个print函数，完成结果打印工作。

    // 在类之间共享数据
    // 由于QueryResult所需要的数据都保存在一个TextQuery对象中，我们就必须确定如何访问它们。我们可以拷贝行号的set，但这样做可能很耗时。
    //   而且，我们当然不希望拷贝vector，因为这可能会引起整个文件的拷贝，而目标只不过是为了打印文件的一小部分而已（通常会是这样）。
    // 通过返回指向TextQuery对象内部的迭代器（或指针），我们可以避免拷贝操作。但是，这种方法开启了一个陷阱：如果TextQuery对象在对应的QueryResult对象之前被销毁，会发生什么？
    //   在此情况下，QueryResult就将引用一个不再存在的对象中的数据。
    // 考虑到这两个类概念上“共享”了数据，可以使用shared_ptr（参见12.1.1节，第400页）来反映数据结构中的这种共享关系。

    // 使用TextQuery类
    // 当我们设计一个类时，在真正实现成员之前先编写程序使用这个类，是一种非常有用的方法。通过这种方法，可以看到类是否具有我们所需要的操作。
    /**
void runQueries(ifstream &infile)
{
    // infile是个ifstream，指向我们要处理的文件
    TextQuery tq(infile); // 保存文件并建立查询map
    // 与用户交互：提示用户输入要查询的单词，完成查询并打印结果
    while (true)
    {
        cout << "enter word to look for, or q to quit: ";
        string s;
        // 若遇到了文件尾或用户输入q，则退出循环
        if (!(cin >> s) || s == "q") break;
        // 指向查询并打印结果
        print(cout, tq.query(s)) << endl;
    }
}
    */

    // 12.3.2 文本查询程序类的定义
    // 设计类的数据成员时，需要考虑与QueryResult对象共享数据的需求。QueryResult类需要共享保存输入文件的vector和保存单词关联的行号的set。因此，这个类应该有两个数据成员：
    //   一个指向动态分配的vector（保存输入文件）的shared_ptr和一个string到shared_ptr<set>的map。map将文件中每个单词关联到一个动态分配的set上，而此set保存了该单词所出现的行号。

    // TextQuery构造函数
    // TextQuery的构造函数接受一个ifstream，逐行读取输入文件：

    // QueryResult类
    // QueryResult类有三个数据成员：一个string，保存查询单词；一个shared_ptr，指向保存输入文件的vector；一个shared_ptr，指向保存单词出现行号的set

    // query函数
    // query函数接受一个string参数，即查询单词，query用它来在map中定位对应的行号set。如果找到了这个string，query函数构造一个QueryResult，保存给定string、TextQuery的file成员以及从wm中提取的set。

    // 打印结果
    // print函数在给定的流上打印出给定的QueryResult对象：


    return 0;
}